﻿/******************************************************************************
 * Project: Processor Affinity Manager                                        *
 * File:    ConfigurationFile.cs                                              *
 * Author:  Aaron Jones                                                       *
 * Version: 1                                                                 *
 *                                                                            *
 * Description:                                                               *
 *    This file contains the implementation for the configuration file used   *
 *    by the project.  The configuration file is a self-persisting object     *
 *    and does not require client/usser intervention to save and load it.     *
 ******************************************************************************/

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Xml;
using System.Xml.Serialization;

namespace ProcessorAffinityManager
{
    /// <summary>
    /// Encapsulates all operations regarding the Processor Affinity Manager
    /// Configuration File.
    /// </summary>
    public class ConfigurationFile
    {
        #region Filename Management
        private static readonly ManagedRegistryKey InstallLocation = new ManagedRegistryKey(
                Registry.LocalMachine, @"SOFTWARE\ProcessorAffinityManager");
        private static string m_ConfigFileName = "PAMConfig.xml";

        /// <summary>
        /// The full path to the directory where the Processor Affinity Manager is installed.
        /// </summary>
        public static string InstallDirectory
        {
            get
            {
                return (string)InstallLocation.GetValue(
                    null, Microsoft.Win32.RegistryValueKind.String);
            }

            set
            {
                InstallLocation.SetValue(value);
            }
        }

        private static string ConfigFileFullPath
        {
            get
            {
                return InstallDirectory + m_ConfigFileName;
            }
        }

        /// <summary>
        /// Remove the config file.
        /// </summary>
        public static void Uninstall()
        {
            System.IO.File.Delete(ConfigFileFullPath);
            InstallLocation.Delete();
        }
        #endregion

        #region File Modification Notification
        private FileSystemWatcher m_Watcher;

        /// <summary>
        /// Deletate for configuration file change handling.
        /// </summary>
        public delegate void ConfigurationChangedEvent();

        /// <summary>
        /// Fired when the configuration file changes.
        /// </summary>
        public event ConfigurationChangedEvent Changed = null;
        
        private void FileChangedEvent(object sender, FileSystemEventArgs e)
        {
            System.Diagnostics.EventLog.WriteEntry("BLAH", "FILE CHANGED");
            Load();

            if (Changed != null)
                Changed();
        }
        #endregion

        #region Default Management
        private List<Default> m_Defaults;

        /// <summary>
        /// A read-only iterable list represnting the processor affinity
        /// defaults configured.
        /// </summary>
        public ReadOnlyCollection<Default> Defaults
        {
            get
            {
                return m_Defaults.AsReadOnly();
            }
        }

        /// <summary>
        /// Add a processor affinity default to the configuration.
        /// </summary>
        /// <param name="newDefault">The default to add to the configuration.</param>
        public void AddDefault(Default newDefault)
        {
            // If a default already exists for this executable
            // then just update its existing entry.
            foreach (Default currentDefault in m_Defaults)
            {
                if (currentDefault.ExecutablePath == newDefault.ExecutablePath)
                {
                    currentDefault.ProcessorAffinity = newDefault.ProcessorAffinity;
                    Save();
                    return;
                }
            }

            m_Defaults.Add(newDefault);
            Save();
        }

        /// <summary>
        /// Remove a default from the processor affinity default configuration.
        /// </summary>
        /// <param name="removalDefault">The default to remove from the configuration.</param>
        public void RemoveDefault(Default removalDefault)
        {
            for (int index = 0; index < m_Defaults.Count; index++)
            {
                if (m_Defaults[index].ExecutablePath == removalDefault.ExecutablePath)
                {
                    m_Defaults.RemoveAt(index--);
                    Save();
                    return;
                }
            }

            throw new ArgumentException("Couldn't remove default \"" + removalDefault.ExecutablePath + "\"" +
                " because it does not exist!");
        }

        /// <summary>
        /// Clear all defaults from the configuration file and save the file.
        /// </summary>
        public void Clear()
        {
            m_Defaults.Clear();
            Save();
        }
        #endregion

        #region Serialization Management
        private TimeSpan m_MaxRetryTime = new TimeSpan(0, 0, 20);
        private TimeSpan m_RetryDelay = new TimeSpan(0, 0, 0, 0, 100);

        private void Save()
        {
            StreamWriter writer = null;

            try
            {
                writer = (StreamWriter)LockFile(true);
                XmlSerializer serializer = new XmlSerializer(m_Defaults.GetType());
                serializer.Serialize(writer, m_Defaults);
                writer.Flush();
                writer.Close();
            }

            catch (TimeoutException)
            {
                // TODO Catch this.
            }

            catch (Exception)
            {
                // TODO Catch this.
            }

            finally
            {
                if (writer != null)
                    writer.Close();
            }
        }

        private void Load()
        {
            StreamReader reader = null;

            try
            {
                reader = (StreamReader)LockFile(false);
                XmlSerializer serializer = new XmlSerializer(m_Defaults.GetType());
                object defaults = serializer.Deserialize(reader);
                reader.Close();

                // FIXME
                // Couldn't find a way to cast to the m_Defaults type generically...
                m_Defaults = (List<Default>)defaults;
            }

            catch (FileNotFoundException)
            {
                Save(); // Start a new file.
            }

            catch (InvalidOperationException)
            {
                // TODO Catch This.
            }

            catch (TimeoutException)
            {
                // TODO Catch This.
            }

            catch (ArgumentException)
            {
                System.Windows.Forms.MessageBox.Show(
                    @"Processor Affinity Manger has an installation issue, please reinstall the software.");
                throw new Exception("Fatal Error");
            }

            finally
            {
                if (reader != null)
                    reader.Close();
            }
        }

        private object LockFile(bool write)
        {
            const int minDelayMilliseconds = 10;
            const int maxDelayMilliseconds = 200;

            DateTime received = DateTime.Now;
            TimeSpan elapsed = new TimeSpan(0);
            TimeSpan maxWaitTime = new TimeSpan(0, 0, 5);

            if(!write && !File.Exists(ConfigFileFullPath))
                throw new FileNotFoundException("The config file does not exist to be read from!");

            while (elapsed < maxWaitTime)
            {
                try
                {
                    if (write)
                    {
                        StreamWriter writer = new StreamWriter(ConfigFileFullPath);
                        return (object)writer;
                    }
                    else
                    {
                        StreamReader reader = new StreamReader(ConfigFileFullPath);
                        return (object)reader;
                    }
                }

                catch (IOException)
                {
                    System.Threading.Thread.Sleep(
                        new TimeSpan(0, 0, 0, 0, new Random((int)received.Ticks).Next(
                            minDelayMilliseconds, maxDelayMilliseconds)));
                    elapsed = DateTime.Now - received;
                }
            }

            throw new TimeoutException("Timed out trying to lock configuration file!");
        }
        #endregion

        #region Construction
        /// <summary>
        /// Default constructor.  Automatically loads the file from persistent storage
        /// and begins watching the file for changes.
        /// </summary>
        public ConfigurationFile()
        {
            m_Defaults = new List<Default>();
            Load();

            m_Watcher = new FileSystemWatcher(InstallDirectory, m_ConfigFileName);
            m_Watcher.Changed += new FileSystemEventHandler(FileChangedEvent);
            m_Watcher.NotifyFilter = NotifyFilters.LastWrite;
            m_Watcher.EnableRaisingEvents = true;
        }
        #endregion
    }
}
